/*
 * webTest.c
 *
 *   Copyright 2010 Emediate ApS
 *
 *   This file is part of Stampede.
 *
 *   Stampede is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Stampede is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with Stampede.  If not, see <http://www.gnu.org/licenses/>.
 *
 *   Written by: Erland Lewin <erland@emediate.se>
 *
 *  Created on: Dec 23, 2009
 */

#include <assert.h>
#include <stdlib.h> /* for rand() */
#include <string.h> /* for memcmp */
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
// #include <sys/timerfd.h> /* Not supported in Debian Lenny */

#include <curl/curl.h>

#include "config.h"
#include "session.h"
#include "transaction.h"
#include "webTest.h"

/*
 * typedefs
 */
typedef int Boolean;

typedef struct sTest
{
   unsigned long timeMin;
   unsigned long timeMax;
   unsigned long timeSum;

   int transCount;
   CURLM *multi_handle;
   Configuration config;
   int maxConcurrent;
   int addedTransactions;
   int logCookies;
   int errorCount;
} sTest, *Test;

typedef struct sUser
{
   Test test;

   int index;
   int sessionIndex;
   Session session;

   int transactionIndex;
   Transaction transaction;
   struct timeval transactionStartTime;
   int transactionByte; // how many bytes have been received.

   CURL *easyhandle;
   struct curl_slist *headerlist;

   /* Thinktime stuff */
   struct timeval endOfWaitTime;
   struct sUser *nextWaitingUser;
} sUser, *User;

/*
 * Constants
 */
/* STAGGERED_USER_START_DELAY_US is the delay between each started user, in
 * order to let the service tested get up to speed. 100ms starts ten users per
 * second
 */
const int STAGGERED_USER_START_DELAY_US = 106033; // a prime number

#define FALSE 0
#define TRUE !FALSE

#define DEBUG 0
#if DEBUG
#define DEBUG_PRINT printf
#else
#define DEBUG_PRINT if(0) printf
#endif

/*
 * static function prototypes
 */
static int socketFunction(CURL *easy,      /* easy handle */
                          curl_socket_t s, /* socket */
                          int action,      /* see values below */
                          void *userp,    /* private callback pointer */
                          void *socketp); /* private socket pointer */
static  size_t curlWriteFunc(void *ptr, size_t size, size_t nmemb, User user );
static void user_startSession( User user, Boolean delayStart );
static void timeval_add_us( struct timeval *timeval, unsigned int us );

static int timeval_subtract( struct timeval *result, const struct timeval *x,
                             const struct timeval *y);
static int timeval_compare( const struct timeval *t1, const struct timeval *t2 );

static void user_startTransaction( User user );
static void user_transactionDone( User user, struct timeval *endTime );
static void doMultiPerform( Test test );
static void startNextTransaction( User user );
static void validate_waitList();

/*
 * Global variable declarations
 */
User firstWaitingUser = NULL;
long bins[ 10000 ];

/*
 * start of code
 *
 * Returns the number of errors
 */
int webTest_run( Configuration config, int userCount, int durationSeconds,
                 int logCookies )
{
   int userIndex;
   sUser *users;
   CURLM *multi_handle;
   fd_set fdread;
   fd_set fdwrite;
   fd_set fdexcep;
   int maxfd, rc;
   struct timeval timeout;
   sTest test = { 1e9, 0, 0, 0, NULL, config, 0, 0, 0 };
   User *latestUser;

   /*
    * bins for calculating an approximate median. Bins are 100 us wide, up to 1s
    */

   users = malloc( sizeof( sUser ) * userCount );
   assert( users != NULL );
   time_t startTime;

   bzero( bins, sizeof( bins ));

   /* Start the sessions */
   test.multi_handle = multi_handle = curl_multi_init();
   test.maxConcurrent = 0;
   test.logCookies = logCookies;

   curl_multi_setopt( multi_handle, CURLMOPT_SOCKETFUNCTION, socketFunction );

   /*
    * First try, loop through sessions
    */
   latestUser = &firstWaitingUser;

   for( userIndex = 0; userIndex < userCount; userIndex++ )
   {
      users[ userIndex ].test = &test;
      users[ userIndex ].sessionIndex = 0;
      users[ userIndex ].index = userIndex;

      // For staggered startup, put all users in the waiting queue

      switch( userIndex )
      {
         case 0:
#if DEBUG
            gettimeofday( &now, NULL );
            printf( "First user session started at %d.%06d\n",
                    (int) now.tv_sec, (int) now.tv_usec );
#endif
            break;

         case 1:
            firstWaitingUser = &(users[ userIndex ]);
            firstWaitingUser->nextWaitingUser = NULL;
            gettimeofday( &(firstWaitingUser->endOfWaitTime), NULL );
            timeval_add_us( &(firstWaitingUser->endOfWaitTime), STAGGERED_USER_START_DELAY_US );
#if DEBUG
            printf( "User 1 scheduled for %d.%06d\n",
                  (int) firstWaitingUser->endOfWaitTime.tv_sec,
                        (int) firstWaitingUser->endOfWaitTime.tv_usec );
#endif
            user_startSession( &(users[ userIndex ]), TRUE );
            break;

         default:
            users[ userIndex - 1 ].nextWaitingUser = &(users[ userIndex ]);
            users[ userIndex ].endOfWaitTime = users[ userIndex - 1 ].endOfWaitTime;
            users[ userIndex ].nextWaitingUser = NULL;
            timeval_add_us( &(users[ userIndex ].endOfWaitTime), STAGGERED_USER_START_DELAY_US );
#if DEBUG
            printf( "User %d scheduled for %d.%06d\n", userIndex,
                  (int) users[ userIndex ].endOfWaitTime.tv_sec,
                        (int) users[ userIndex ].endOfWaitTime.tv_usec );
#endif
            user_startSession( &(users[ userIndex ]), TRUE );
            break;
      }
   }

   validate_waitList();

   user_startSession( &(users[ 0 ]), FALSE );
   doMultiPerform( &test );

   /* set a suitable timeout to play around with */
   timeout.tv_sec = 1;
   timeout.tv_usec = 100;

   startTime = time( NULL );

   do
   {
      struct timeval now;

      /* If there is a waiting user, set the timeout to the time when that
       * user should wake up */
      if( firstWaitingUser != NULL )
      {
         int diff, userStarted = 0;

         gettimeofday( &now, NULL );

         do
         {
            gettimeofday( &now, NULL );
#if DEBUG
            printf( "now=%d.%06d, eoW=%d.%06d\n", (int) now.tv_sec,
                  (int) now.tv_usec,
                  (int) firstWaitingUser->endOfWaitTime.tv_sec,
                  (int) firstWaitingUser->endOfWaitTime.tv_usec );
#endif
            // diff true if endOfWaitTime < now
            diff = timeval_subtract ( &timeout, &(firstWaitingUser->endOfWaitTime), &now);

            if( diff > 0 )
            {
               User user;

               user = firstWaitingUser;
               firstWaitingUser = user->nextWaitingUser;

               validate_waitList();

               DEBUG_PRINT( "Waking up user %d\n", user->index );

               // A user is already ready to start
               startNextTransaction( user );
               userStarted++;
            }
         } while( firstWaitingUser != NULL && (diff > 0) );

         if( userStarted )
         {
            doMultiPerform( &test );
            continue;
         }
         if( firstWaitingUser == NULL )
            timeout.tv_sec = 1; // 1 second timeout if no-one is waiting
      }
      else
         timeout.tv_sec = 1;

#if DEBUG
      printf( "select: timeout=%d.%06d secs\n", (int) timeout.tv_sec, (int) timeout.tv_usec );
#endif
      /* get file descriptors from the transfers */
      FD_ZERO(&fdread);
      FD_ZERO(&fdwrite);
      FD_ZERO(&fdexcep);

      curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd);

      rc = select(maxfd+1, &fdread, &fdwrite, &fdexcep, &timeout);

      switch(rc) {
         case -1:
            perror( "select error"); /* select error */
            break;

         case 0:
            gettimeofday( &now, NULL );

            if( firstWaitingUser != NULL )
            {
               int diff;

               // timeout, start a user if one is waiting
               do
               {
                  struct timeval *tv2 = &(firstWaitingUser->endOfWaitTime);
                  diff = timeval_subtract ( &timeout, tv2, &now );
#if DEBUG
                  printf( "user %d, %d.%06d-%d.%06d=%d.%06d, diff=%d\n", firstWaitingUser->index,
                          (int) now.tv_sec, (int) now.tv_usec, (int) tv2->tv_sec, (int) tv2->tv_usec,
                          (int) timeout.tv_sec, (int) timeout.tv_usec, diff );
#endif
                  if( diff > 0 )
                  {
                     User user;

                     user = firstWaitingUser;
#if DEBUG
                     printf( "%d.%06d select timeout: Waking up user %d\n",
                             (int) now.tv_sec, (int) now.tv_usec, user->index );
#endif
                     firstWaitingUser = user->nextWaitingUser;

                     validate_waitList();

                     // A user is already ready to start
                     startNextTransaction( user );
#if DEBUG
                     printf( "wakeup: afterStartNextTransaction: addedTransactions=%d\n", test.addedTransactions );
#endif
                  }
               } while( firstWaitingUser != NULL && (diff > 0) );
            }
#if DEBUG
            printf( "wakeup: addedTransactions=%d\n", test.addedTransactions );
#endif
            if( test.addedTransactions )
               doMultiPerform( &test );

            break;

         default:
            /* readable/writable sockets */
#if 0
            multiPerformCode = curl_multi_socket_action( multi_handle,
                                                curl_socket_t sockfd, int ev_bitmask,
                                                int *running_handles);
#endif
            doMultiPerform( &test );
            break;
      }
   } while( time( NULL ) < (startTime + durationSeconds) );

   /* calculate median */
   int binIndex;
   long binSum;

   for( binIndex = 0, binSum = 0; binSum < test.transCount / 2; binIndex++ )
   {
      binSum += bins[ binIndex ];
   }

   printf( "Web test result: %d errors, %d transactions (max %d concurrent), min=%ld us, "
           "avg=%ld us, max=%ld us, median = ca %d us\n", test.errorCount,
           test.transCount, test.maxConcurrent, test.timeMin,
           test.timeSum / test.transCount, test.timeMax, binIndex * 100 );

   curl_multi_cleanup(multi_handle);

   return test.errorCount;
}

static void doMultiPerform( Test test )
{
   CURLcode multiPerformCode;

   do
   {
      CURLMsg *msg;
      int msgCount;
      struct timeval tv;
      User user;
      CURLcode curlCode;
      int still_running;
#if 0
      multiPerformCode curl_multi_socket_action( multi_handle,
                                          curl_socket_t sockfd, int ev_bitmask,
                                          int *running_handles);
#endif
      test->addedTransactions = 0;
      multiPerformCode = curl_multi_perform( test->multi_handle, &still_running);
      if( still_running > test->maxConcurrent )
         test->maxConcurrent = still_running;
#if DEBUG
      {
         struct timeval now;

         gettimeofday( &now, NULL );

         printf( "%d.%06d curl_multi_perform: still_running=%d, code=%s\n", (int) now.tv_sec, (int) now.tv_usec,
                 still_running, curl_multi_strerror( multiPerformCode ) );
      }
#endif
      while( 1 )
      {
         msg = curl_multi_info_read( test->multi_handle, &msgCount );
         if( msg == NULL )
            break;

         switch( msg->msg )
         {
            case CURLMSG_DONE:
               gettimeofday( &tv, NULL ); // do this as early as possible to get accurate timing

               // Get the user of the transfer
               curlCode = curl_easy_getinfo( msg->easy_handle, CURLINFO_PRIVATE, (char **) &user );
               assert( curlCode == CURLE_OK );

               user_transactionDone( user, &tv );
               break;

            default: // ignore anything else
               printf( "WARNING: got msg %d\n", msg->msg );
               break;
         }
      }

   } while( multiPerformCode == CURLM_CALL_MULTI_PERFORM || test->addedTransactions );

#if DEBUG
      {
         struct timeval now;

         gettimeofday( &now, NULL );

         printf( "%d.%06d curl_multi_perform: leaving\n", (int) now.tv_sec, (int) now.tv_usec );
      }
#endif

}

static void user_startSession( User user, int delayTransaction )
{
   int i;   Transaction transaction;
   CURL *curl;
   Test test = user->test;
   Session session;

   curl = curl_easy_init();
   user->transactionIndex = 0;

   /* select a random session */
   user->sessionIndex = rand() % config_getSessionCount( test->config );
#if 0
   user->sessionIndex++;
   if( user->sessionIndex >= config_getSessionCount( test->config ) )
      user->sessionIndex = 0;
#endif
   session = user->session = config_getSessionByIndex( test->config, user->sessionIndex );

   transaction = session_getTransactionByIndex( session, 0 );
   user->session = session;
   user->transaction = transaction;
   user->easyhandle = curl;

   /* no progress meter please */
   curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
   curl_easy_setopt(curl, CURLOPT_PRIVATE, user );
   // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1 );
   curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "" ); // enable cookie handling

   if( transaction_getExpectedBody( user->transaction ) != NULL )
   {
      /* send all data to this function  */
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback) curlWriteFunc );
      /* send all data to this function  */
      curl_easy_setopt(curl, CURLOPT_WRITEDATA, user );
   }

   user->headerlist = NULL;

   /* set up the HTTP headers for the session */
   for( i = 0; i < session_getHeaderCount(session); i++ )
   {
      const xmlChar *headerString;

      headerString = session_getHeaderStringByIndex( session, i );

      user->headerlist = curl_slist_append( user->headerlist, (char *) headerString);
   }

   curl_easy_setopt( curl, CURLOPT_HTTPHEADER, user->headerlist );

   if( !delayTransaction )
      user_startTransaction( user );
   else
      user->transactionIndex = -1;
}

static void user_startTransaction( User user )
{
   char *url;
   CURL *curl;
   CURLM *multi_handle;
   CURLMcode code;
   Transaction transaction;

   curl = user->easyhandle;
   multi_handle = user->test->multi_handle;
   transaction = user->transaction;

   user->transactionByte = 0;
   url = (char *) transaction_getURL( transaction );
   curl_easy_setopt( user->easyhandle, CURLOPT_URL, url );

   gettimeofday( &(user->transactionStartTime), NULL );

   /*
    * If cookie logging is on, then read the cookie values and print them
    */
   if( user->test->logCookies )
   {
      struct curl_slist *cookieList;
      struct curl_slist *current;
      curl_easy_getinfo( curl, CURLINFO_COOKIELIST, &cookieList );

      printf( "%d.%06d: user %d started session '%s', transaction %d: call cookies:",
              (int) user->transactionStartTime.tv_sec,
              (int) user->transactionStartTime.tv_usec, user->index,
              (char *) session_getName( user->session ), user->transactionIndex );

      if( cookieList == NULL )
         printf( "None\n" );
      else
      {
         for( current = cookieList; current != NULL; current = current->next )
            printf( "\n%s", current->data );

         curl_slist_free_all( cookieList );
      }
   }

   code = curl_multi_add_handle( multi_handle, curl );
   assert( code == CURLE_OK );
   user->test->addedTransactions++;

#if DEBUG
   printf( "%d.%06d: user %d started session '%s', transaction %d.\n",
           (int) user->transactionStartTime.tv_sec,
           (int) user->transactionStartTime.tv_usec, user->index,
           (char *) session_getName( user->session ), user->transactionIndex );
#endif
}

static  size_t curlWriteFunc(void *ptr, size_t size, size_t nmemb, User user )
{
   char *inptr = (char *) ptr;
   const char *wantedPtr = (char *) transaction_getExpectedBody( user->transaction );
   int byteCount = size * nmemb;

   if( wantedPtr != NULL )
   {
      wantedPtr += user->transactionByte;

      if( memcmp( inptr, wantedPtr, byteCount ) != 0 )
      {
         printf( "user %d, trans %d: error\n", user->index, user->transactionIndex );

         fprintf( stderr, "ERROR: mismatch between actual and wanted." );
         fprintf( stderr, "Session %s, Transaction %d:\n",
                  session_getName( user->session ), user->transactionIndex );
         fprintf( stderr, "Wanted:\n%s\nGot:\n%s\n",
                  (char *) transaction_getExpectedBody( user->transaction ),
                  (char *) ptr );
         user->test->errorCount++;
      }
      user->transactionByte += byteCount;
   }

   return byteCount;
}

static int socketFunction(CURL *easy,      /* easy handle */
                          curl_socket_t s, /* socket */
                          int action,      /* see values below */
                          void *userp,    /* private callback pointer */
                          void *socketp) /* private socket pointer */
{

   return 0;
}

static void user_transactionDone( User user, struct timeval *endTime )
{
   struct timeval dt;
   long microsecs;
   Test test;
   Session session;
   int thinkTime;
   int bin;
   struct curl_slist *cookieList;

   test = user->test;
   session = user->session;

   if( test->logCookies )
      curl_easy_getinfo( user->easyhandle, CURLINFO_COOKIELIST, &cookieList );

   curl_multi_remove_handle( test->multi_handle, user->easyhandle );

   if( test->logCookies )
   {
      struct curl_slist *current;

      printf( "%d.%06d: user %d session '%s', ended transaction %d: cookies:",
              (int) endTime->tv_sec,
              (int) endTime->tv_usec, user->index,
              (char *) session_getName( user->session ), user->transactionIndex );

      if( cookieList == NULL )
         printf( "None\n" );
      else
      {
         for( current = cookieList; current != NULL; current = current->next )
            printf( "\n%s", current->data );

         curl_slist_free_all( cookieList );
      }
   }
   // calculate statistics.
   timeval_subtract( &dt, endTime, &(user->transactionStartTime) );

   microsecs = dt.tv_usec + dt.tv_sec * 1e6;

#if DEBUG
   printf( "user_transactionDone: user %d, %ld usecs\n", user->index, microsecs );
#endif
   bin = microsecs / 100;
   if( bin >= 10000 )
      bin = 9999;

   bins[ bin ]++;

   if( microsecs < test->timeMin )
      test->timeMin = microsecs;
   if( microsecs > test->timeMax )
      test->timeMax = microsecs;

   test->timeSum += microsecs;

   test->transCount++;

   thinkTime = transaction_getThinkTime( user->transaction );

   if( thinkTime > 0 )
   {
      struct timeval now;
      User *prevUser;

      gettimeofday( &now, NULL );

      // calculate time when delay done
      user->endOfWaitTime.tv_sec = now.tv_sec + thinkTime;
      user->endOfWaitTime.tv_usec = now.tv_usec;

      // insert into the linked list of waiting users
      prevUser = &firstWaitingUser;
      while( *prevUser != NULL && (timeval_compare( &(user->endOfWaitTime),
            &((*prevUser)->endOfWaitTime)) > 0) )
         prevUser = &((*prevUser)->nextWaitingUser);

      if( *prevUser != NULL )
      {
         user->nextWaitingUser = *prevUser;
         *prevUser = user;
      }
      else
      {
         // insert last in list
         *prevUser = user;
         user->nextWaitingUser = NULL;
      }
      validate_waitList();
   }
   else
      startNextTransaction( user );
}

static void validate_waitList()
{
   User user;

   for( user = firstWaitingUser; user != NULL; user = user->nextWaitingUser )
   {
      if( user->nextWaitingUser != NULL )
      {
         User nextUser = user->nextWaitingUser;

         assert( nextUser->endOfWaitTime.tv_sec >= user->endOfWaitTime.tv_sec );
         if( nextUser->endOfWaitTime.tv_sec == user->endOfWaitTime.tv_sec )
            assert( nextUser->endOfWaitTime.tv_usec >= user->endOfWaitTime.tv_usec );
      }
   }
}

static void startNextTransaction( User user )
{
   Session session = user->session;

   // start the next transaction
   user->transactionIndex++;

   if( user->transactionIndex >= session_getTransactionCount( session ) )
   {
      // end the previous session
      curl_easy_cleanup( user->easyhandle );
      curl_slist_free_all( user->headerlist );
      user->headerlist = NULL;
      user->easyhandle = NULL;

#if DEBUG
      printf( "startNextTransaction: user %p: session completed, starting new session.\n", user );
#endif
      user_startSession( user, FALSE  );
   }
   else
   {
#if DEBUG
      printf( "startNextTransaction: user %p: starting next transaction.\n", user );
#endif
      user->transaction = session_getTransactionByIndex( session, user->transactionIndex );
      user_startTransaction( user );
   }
}

/* return <0 if t1 < t2,
 *        >0 if t2 > t1
 *        =  if t1 == t2
 */
static int timeval_compare( const struct timeval *t1, const struct timeval *t2 )
{
   if( t1->tv_sec < t2->tv_sec )
      return -1;
   else if( t1->tv_sec > t2->tv_sec )
      return 1;
   else if( t1->tv_usec < t2->tv_usec)
      return -1;
   else if( t1->tv_usec > t2->tv_usec)
      return 1;
   else
      return 0;
}

/* Subtract the `struct timeval' values X and Y,
   storing the result in RESULT.
   Return 1 if the difference is negative, otherwise 0.

   From http://www.gnu.org/s/libc/manual/html_node/Elapsed-Time.html
    */

static int timeval_subtract( struct timeval *result, const struct timeval *x2,
                             const struct timeval *y2)
{
   struct timeval x3, y3;
   struct timeval *x = &x3, *y=&y3;
   x3 = *x2;
   y3 = *y2;

  /* Perform the carry for the later subtraction by updating y. */
  if (x->tv_usec < y->tv_usec) {
    int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1;
    y->tv_usec -= 1000000 * nsec;
    y->tv_sec += nsec;
  }
  if (x->tv_usec - y->tv_usec > 1000000) {
    int nsec = (x->tv_usec - y->tv_usec) / 1000000;
    y->tv_usec += 1000000 * nsec;
    y->tv_sec -= nsec;
  }

  /* Compute the time remaining to wait.
     tv_usec is certainly positive. */
  result->tv_sec = x->tv_sec - y->tv_sec;
  result->tv_usec = x->tv_usec - y->tv_usec;

  /* Return 1 if result is negative. */
  return x->tv_sec < y->tv_sec;
}

static void timeval_add_us( struct timeval *timeval, unsigned int us )
{
   unsigned long new_us;

   new_us = timeval->tv_usec + us;
   if( new_us >= 1000000 )
   {
      unsigned long ds;

      ds = new_us / 1000000;
      timeval->tv_sec += ds;
      timeval->tv_usec = new_us - ds * 1000000;
   }
   else
      timeval->tv_usec = new_us;
}
